package com.king.armips.android;

import android.app.AlertDialog;
import android.content.Intent;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
import android.text.InputFilter;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.Nullable;

import com.king.armips.ArgumentsParseException;
import com.king.armips.ArmipsArguments;
import com.king.armips.ArmipsDroid;
import com.king.armips.android.activity.DocActivity;
import com.king.armips.android.base.BaseActivity;
import com.king.armips.android.configs.AppConfigs;
import com.king.armips.android.dialog.LoadingDialog;
import com.king.armips.android.dialog.SidebarDialog;
import com.king.armips.android.utils.FileUtils;
import com.king.armips.android.utils.IoHandler;
import com.king.armips.android.utils.MainHandler;
import com.king.armips.android.utils.ScreenUtils;
import com.king.armips.android.utils.SoftKeyBoardUtils;
import com.king.armips.android.widget.HexDataView;

import java.io.File;
import java.util.HashSet;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import io.github.rosemoe.editor.widget.CodeEditor;

public class MainActivity extends BaseActivity {
    private static final String TAG = MainActivity.class.getSimpleName();

    private static final HashSet<String> CODE_FILE_FORMATS = new HashSet<String>() {{
        add(".s");
        add(".asm");
        add(".txt");
    }};

    private static final Pattern HEX_REGEX = Pattern.compile("^[0-9a-fA-F]+$");

    private MainHandler mainHandler = new MainHandler();
    private IoHandler ioHandler = new IoHandler();

    private ArmipsDroid armipsDroid = new ArmipsDroid();

    private long exitTimestamp = 0;

    private TextView tvFilename;

    private View btnAssembly;
    private View btnAssemblyOptions;
    private View btnGotoOffset;

    private CodeEditor codeEditor;
    private HexDataView hexDataView;
    private TextView tvNoFile;

    private SidebarDialog sidebarDialog;
    private AlertDialog assemblyConfigDialog;
    private LoadingDialog loadingDialog;
    private AlertDialog messageDialog;
    private AlertDialog gotoOffsetDialog;

    private EditText etAssemblyParameters;
    private EditText etGotoOffset;

    private File workDir;
    private File currentFile;

    private AtomicBoolean isCodeSaving = new AtomicBoolean(false);
    private boolean isChangeCodeFromSystem = true;
    private Runnable codeSaveRunnable = null;
    private Runnable assemblyRunnable = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ScreenUtils.init(this);
        AppConfigs.init(this);

        tvNoFile = findViewById(R.id.tv_no_file);
        tvFilename = findViewById(R.id.tv_filename);
        btnAssembly = findViewById(R.id.btn_assembly);
        btnAssemblyOptions = findViewById(R.id.btn_assembly_options);
        btnGotoOffset = findViewById(R.id.btn_goto_offset);

        loadingDialog = new LoadingDialog(this);

        initCodeEditor();
        initHexEditor();

        btnAssembly.setOnClickListener(v -> startAssemblyTask());

        btnAssemblyOptions.setOnClickListener(v -> showAssemblyConfigDialog());

        btnGotoOffset.setOnClickListener(v -> showGotoOffsetDialog());

        findViewById(R.id.btn_menu).setOnClickListener(v -> showSidebarDialog());

        workDir = new File(getCacheDir(),"armips");

        if (!workDir.exists()){
            workDir.mkdirs();
        }

        loadLastOpenedFile();
    }

    private void loadLastOpenedFile(){
        String lastOpenedFilePath = AppConfigs.getLastOpenedFilePath();
        File lastFile = null;
        if (lastOpenedFilePath != null){
            File file = new File(workDir,lastOpenedFilePath);
            if (file.exists()) {
                lastFile = file;
            }
        }

        if (lastFile == null){
            String filePath = "test.asm";
            File file = new File(workDir,filePath);
            if (!file.exists()){
                FileUtils.writeText(file,AppConfigs.getTestCode());
            }
            loadingFile(filePath,true);
        }
        else {
            boolean isCode = CODE_FILE_FORMATS.contains(FileUtils.getSuffix(lastOpenedFilePath));
            loadingFile(lastOpenedFilePath,isCode);
        }
    }

    private void initCodeEditor(){
        codeEditor = findViewById(R.id.code_editor);

        codeEditor.setTypefaceText(Typeface.MONOSPACE);

        codeEditor.setOverScrollEnabled(false);
        codeEditor.setPinLineNumber(true); //设置固定行号区域
//        codeEditor.setScalable(false); //禁止双指缩放

        codeEditor.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) { }
            @Override
            public void afterTextChanged(Editable s) {
                if (!isChangeCodeFromSystem){
                    autoSaveCode(s.toString());
                }
            }
        });

    }

    private void initHexEditor(){
        hexDataView = findViewById(R.id.hex_data_view);
        hexDataView.setVisibility(View.GONE);
    }

    private void showSidebarDialog(){
        if (sidebarDialog == null){
            sidebarDialog = new SidebarDialog(this);
            sidebarDialog.setBaseDir(workDir);

            sidebarDialog.setOnItemClickListener(new SidebarDialog.OnItemClickListener() {
                @Override
                public void importFile() {
                    FileUtils.selectFile(getLauncher(),result -> {
                        Intent data = result.getData();
                        if (data != null){
                            Uri uri = data.getData();
                            if (uri != null){
                                String fileName = FileUtils.getFileNameFromUri(mContext, uri);
                                if (TextUtils.isEmpty(fileName)){
                                    Toast.makeText(mContext,R.string.file_import_failed,Toast.LENGTH_SHORT).show();
                                }
                                else {
                                    File dstFile = new File(workDir,fileName);

                                    FileUtils.saveFile(mContext,uri,dstFile);

                                    Toast.makeText(mContext,R.string.file_import_success,Toast.LENGTH_SHORT).show();
                                }

                            }
                        }
                    });
                }

                @Override
                public void onSelected(String filePath) {
                    boolean isCode = CODE_FILE_FORMATS.contains(FileUtils.getSuffix(filePath));
                    loadingFile(filePath,isCode);
                }

                @Override
                public void onRenamed(String name) {
                    tvFilename.setText(name);
                }

                @Override
                public void onDeleted(String name) {
                    tvFilename.setText("");
                    tvNoFile.setVisibility(View.VISIBLE);
                    
                    codeEditor.setVisibility(View.GONE);
                    hexDataView.setVisibility(View.GONE);
                    btnAssembly.setVisibility(View.GONE);
                    btnAssemblyOptions.setVisibility(View.GONE);
                    btnGotoOffset.setVisibility(View.GONE);

                }
            });

        }
        sidebarDialog.refreshFiles();
        sidebarDialog.show();
    }

    private void showGotoOffsetDialog(){
        if (gotoOffsetDialog == null){
            gotoOffsetDialog = new AlertDialog.Builder(this)
                    .setTitle(R.string.goto_data_offset)
                    .setView(R.layout.dialog_edittext)
                    .setNegativeButton(R.string.cancel, (dialog, which) -> { })
                    .setPositiveButton(R.string.ok, null)
                    .create();
            gotoOffsetDialog.create();

            gotoOffsetDialog.setCanceledOnTouchOutside(false);
            Window window = gotoOffsetDialog.getWindow();
            WindowManager.LayoutParams layoutParams = window.getAttributes();
            layoutParams.width = (int) (ScreenUtils.screenWidth *0.75f);
            window.setAttributes(layoutParams);

            etGotoOffset = gotoOffsetDialog.findViewById(R.id.edittext);

            etGotoOffset.setFilters(new InputFilter[]{
                    (source, start, end, dest, dstart, dend) -> {
                        if (source.length() == 0)
                            return null;

                        if (!HEX_REGEX.matcher(source).matches())
                            return "";

                        return null;
                    }
            });

            etGotoOffset.setImeOptions(EditorInfo.IME_ACTION_DONE);
            etGotoOffset.setSingleLine();
            etGotoOffset.setOnEditorActionListener((v, actionId, event) -> {
                if (actionId == EditorInfo.IME_ACTION_DONE){
                    if (!gotoDataOffset())
                        return false;

                    gotoOffsetDialog.dismiss();
                    return true;
                }
                return false;
            });

            gotoOffsetDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
                if (gotoDataOffset()){
                    gotoOffsetDialog.dismiss();
                }
            });

        }

        etGotoOffset.setHint("0.."+Integer.toHexString(hexDataView.getFileSize()-1).toUpperCase());
        gotoOffsetDialog.show();
        etGotoOffset.postDelayed(()-> SoftKeyBoardUtils.showSoftKeyboard(etGotoOffset),150);
    }

    private boolean gotoDataOffset(){
        String text = etGotoOffset.getText().toString();
        int offset = -1;
        try {
            offset = Integer.parseInt(text,16);
        }catch (Exception e){
            e.printStackTrace();
        }
        if (offset == -1){
            Toast.makeText(mContext,R.string.invalid_data_offset,Toast.LENGTH_SHORT).show();
            return false;
        }
        int fileSize = hexDataView.getFileSize();
        if (offset >= fileSize){
            Toast.makeText(mContext,R.string.data_offset_out_of_range,Toast.LENGTH_SHORT).show();
            return false;
        }

        hexDataView.setDataOffset(offset);

        return true;
    }

    private void loadingFile(String filePath,boolean isCode){
        File file = new File(workDir,filePath);
        currentFile = file;
        AppConfigs.setLastOpenedFilePath(filePath);
        tvFilename.setText(file.getName());
        if (isCode){
            btnAssembly.setVisibility(View.VISIBLE);
            btnAssemblyOptions.setVisibility(View.VISIBLE);
            btnGotoOffset.setVisibility(View.GONE);
        }
        else {
            btnAssembly.setVisibility(View.GONE);
            btnAssemblyOptions.setVisibility(View.GONE);
            btnGotoOffset.setVisibility(View.VISIBLE);
        }

        loadingDialog.setMessage(getString(R.string.file_loading));
        loadingDialog.showDelayed(500);
        new Thread(()->{
            String[] codeTextHolder = new String[]{ "" };
            if (isCode){
                codeTextHolder[0] = FileUtils.readText(file);
            }
            else {
                hexDataView.setSrcFile(file);
            }

            mainHandler.post(()->{
                codeEditor.setVisibility(isCode ? View.VISIBLE : View.GONE);
                hexDataView.setVisibility(isCode ? View.GONE : View.VISIBLE);
                tvNoFile.setVisibility(View.GONE);

                isChangeCodeFromSystem = true;
                codeEditor.setText("");
                codeEditor.setTextSize(CodeEditor.DEFAULT_TEXT_SIZE);
                codeEditor.setText(codeTextHolder[0]);
                isChangeCodeFromSystem = false;

                loadingDialog.dismiss();
            });
        }).start();
    }

    private void showAssemblyConfigDialog(){
        if (assemblyConfigDialog == null){
            View rootView = LayoutInflater.from(mContext).inflate(R.layout.dialog_assembly_configuration, null);
            assemblyConfigDialog = new AlertDialog.Builder(mContext)
                    .setTitle(R.string.assembly_configuration)
                    .setView(rootView)
                    .setNegativeButton(R.string.cancel, (dialog, which) -> { })
                    .setPositiveButton(R.string.ok, null)
                    .create();

            assemblyConfigDialog.create();

            assemblyConfigDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
                String parameters = getValidAssemblyParameters();
                if (parameters == null)
                    return;

                AppConfigs.setAssemblyParameters(parameters);

                assemblyConfigDialog.dismiss();
            });

            etAssemblyParameters = rootView.findViewById(R.id.et_assembly_parameters);

            rootView.findViewById(R.id.btn_assembly_parameters_help).setOnClickListener(v -> {
                DocActivity.start(mContext,"armips_readme",true,"armips assembler");
            });

            String assemblyParameters = AppConfigs.getAssemblyParameters();
            etAssemblyParameters.setText(assemblyParameters);
            etAssemblyParameters.setSelection(assemblyParameters.length());

        }
        assemblyConfigDialog.show();
        etAssemblyParameters.postDelayed(()-> SoftKeyBoardUtils.showSoftKeyboard(etAssemblyParameters),150);
    }

    @Nullable
    private String getValidAssemblyParameters(){
        String text = etAssemblyParameters.getText().toString().trim();
        if (text.isEmpty())
            return "";

        ArmipsArguments armipsArguments = null;
        String errorMessage = "";
        try {
            armipsArguments = ArmipsArguments.parseArguments(text);
        }
        catch (ArgumentsParseException e){
            int code = -e.getCode();
            int stringId = getResources().getIdentifier("armips_command_error_" + code, "string", getPackageName());
            if (stringId != 0){
                Object[] errorArgs = e.getArgs();
                if (errorArgs.length != 0){
                    errorMessage = getString(stringId,errorArgs);
                }
                else {
                    errorMessage = getString(stringId);
                }
            }
            else {
                errorMessage = getString(R.string.invalid_assembly_parameters);
            }
        }

        if (armipsArguments == null){
            Toast.makeText(mContext,errorMessage,Toast.LENGTH_SHORT).show();
            return null;
        }

        return text;
    }

    private void startAssemblyTask(){
        final ArmipsArguments arguments;
        try {
            arguments = ArmipsArguments.parseArguments(AppConfigs.getAssemblyParameters());
        }catch (ArgumentsParseException e){
            Toast.makeText(mContext,R.string.invalid_assembly_parameters,Toast.LENGTH_SHORT).show();
            return;
        }

        SoftKeyBoardUtils.hideSoftKeyboard(this);

        loadingDialog.setMessage(null);
        loadingDialog.show();

        Runnable runnable = ()->{

            StringBuffer resultMessageBuffer = new StringBuffer();

            if (hasAsmCode()){

                arguments.inputFileName = currentFile.getAbsolutePath().replace(workDir.getAbsolutePath()+"/","");
                arguments.rootDir = workDir.getAbsolutePath();

                String resultMessage = armipsDroid.runArmips(arguments);

                if (resultMessage != null) {
                    resultMessageBuffer.append(resultMessage);
                }

            }

            mainHandler.post(()->{
                loadingDialog.dismissDelayed(500);
                showAssemblyResultMessage(resultMessageBuffer.toString());
            });
        };

        if (isCodeSaving.get()){
            assemblyRunnable = runnable;
        }
        else {
            ioHandler.postDelayed(runnable,100);
        }
    }

    private boolean hasAsmCode(){
        String text = codeEditor.getText().toString();
        int length = text.length();

        for (int i=0;i<length;i++){
            char c = text.charAt(i);
            if (c == ' ' || c == '\t' || c == '\n' || c == '\r')
                continue;
//            if (c == '.' || c == ':' || c == ';')
//                continue;

            return true;
        }

        return false;
    }

    private void showAssemblyResultMessage(String message){
        if (messageDialog == null){
            messageDialog = new AlertDialog.Builder(mContext)
                    .setTitle(R.string.assembly_completed)
                    .setMessage(message)
                    .setPositiveButton(R.string.ok, null)
                    .create();
            messageDialog.setCanceledOnTouchOutside(false);
        }
        messageDialog.setMessage(message);
        messageDialog.show();
    }

    private void autoSaveCode(String text){
        if (codeSaveRunnable != null){
            ioHandler.removeCallbacks(codeSaveRunnable);
        }
        codeSaveRunnable = ()->{
            FileUtils.writeText(currentFile,text);
            isCodeSaving.set(false);

            if (assemblyRunnable != null){
                assemblyRunnable.run();
            }
            assemblyRunnable = null;
            codeSaveRunnable = null;
        };
        isCodeSaving.set(true);
        ioHandler.postDelayed(codeSaveRunnable,100);
    }

    @Override
    public void onBackPressed() {
        if (hexDataView.getSelectedRange() != null){
            hexDataView.clearSelection();
            return;
        }

        long currentTimeMillis = System.currentTimeMillis();
        if (currentTimeMillis - exitTimestamp > 2000){
            exitTimestamp = currentTimeMillis;
            Toast.makeText(this,R.string.exit_app_tips,Toast.LENGTH_SHORT).show();
            return;
        }
        finish();
    }

}